常见 Virtual Dom 面试题

Author Avatar
Klein 8月 24, 2018

vdom 是什么?为什么会存在 vdom?

Virtual Dom 也称虚拟 DOM ,即使用 JS 模拟 DOM 结构。
众所周知,操作 DOM 是很耗费性能的一件事情,既然如此,我们可以考虑通过 JS 对象来模拟 DOM 对象,毕竟操作 JS 对象比操作 DOM 省时的多。DOM 变化的对比,放到 JS 层来做,提高重绘性能。

举个例子:

1
2
3
4
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
</ul>

上面是一段HTML代码,我们用 JS 来模拟一下它的 DOM 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
tag: 'ul',
attrs: {
id: 'list'
},
children: [
{
tag: 'li',
attrs: {
className: 'item'
},
children: ['Item 1']
},
{
tag: 'li',
attrs: {
className: 'item'
},
children: ['Item 2']
},
]
}

为什么要使用vdom?
设计一个需求场景:

  1. 将数据设计成一个表格。
  2. 随意修改一个信息,表格也随之改变。

使用 jQuery 来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<div id="container"></div>
<button id="btn-change">change</button>
<script type="text/javascript">
var data = [
{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]

// 渲染函数
function render(data) {
var $container = $('#container')

// 清空容器,重要!!!
$container.html('')

// 拼接 table
var $table = $('<table>')

$table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'))
data.forEach(function (item) {
$table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>'))
})

// 渲染到页面
$container.append($table)
}

$('#btn-change').click(function () {
data[1].age = 30
data[2].address = '深圳'
// re-render 再次渲染
render(data)
})

// 页面加载完立刻执行(初次渲染)
render(data)

</script>

在开发者工具可以看到,每一次修改都需要将整个表格推倒重来,重新渲染。而dom操作又是十分昂贵的,为了减少dom操作,需要用JS进行DOM对比,提高效率。

vdom 的如何应用,核心API是什么?

使用 snabbdom 改写上面的情景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<div id="container"></div>
<button id="btn-change">change</button>
<script type="text/javascript">
var snabbdom = window.snabbdom

// 定义 patch
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])

// 定义 h
var h = snabbdom.h

var container = document.getElementById('container')

// 生成 vnode
var vnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item 2')
])
patch(container, vnode)

document.getElementById('btn-change').addEventListener('click', function () {
// 生成 newVnode
var newVnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item B'),
h('li.item', {}, 'Item 3')
])
patch(vnode, newVnode)
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<div id="container"></div>
<button id="btn-change">change</button>

<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.0/h.js"></script>
<script type="text/javascript">
var snabbdom = window.snabbdom
// 定义关键函数 patch
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])

// 定义关键函数 h
var h = snabbdom.h

// 原始数据
var data = [
{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]
// 把表头也放在 data 中
data.unshift({
name: '姓名',
age: '年龄',
address: '地址'
})

var container = document.getElementById('container')

// 渲染函数
var vnode
function render(data) {
var newVnode = h('table', {}, data.map(function (item) {
var tds = []
var i
for (i in item) {
if (item.hasOwnProperty(i)) {
tds.push(h('td', {}, item[i] + ''))
}
}
return h('tr', {}, tds)
}))

if (vnode) {
// re-render
patch(vnode, newVnode)
} else {
// 初次渲染
patch(container, newVnode)
}

// 存储当前的 vnode 结果
vnode = newVnode
}

// 初次渲染
render(data)


var btnChange = document.getElementById('btn-change')
btnChange.addEventListener('click', function () {
data[1].age = 30
data[2].address = '深圳'
// re-render
render(data)
})

</script>

核心 API:

  • h(‘<标签名>’, {…属性…}, […子属性…])
  • h(‘<标签名>’, {…属性…}, ‘…’)
  • patch(vontainer, vnode)
  • patch(vnode, newVnode)

    介绍一下 diff 算法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    function createElement(vnode) {
    var tag = vnode.tag // 'ul'
    var attrs = vnode.attrs || {}
    var children = vnode.children || []
    if (!tag) {
    return null
    }

    // 创建真实的 DOM 元素
    var elem = document.createElement(tag)
    // 属性
    var attrName
    for (attrName in attrs) {
    if (attrs.hasOwnProperty(attrName)) {
    // 给 elem 添加属性
    elem.setAttribute(attrName, attrs[attrName])
    }
    }
    // 子元素
    children.forEach(function (childVnode) {
    // 给 elem 添加子元素
    elem.appendChild(createElement(childVnode)) // 递归
    })

    // 返回真实的 DOM 元素
    return elem
    }

    function updateChildren(vnode, newVnode) {
    var children = vnode.children || []
    var newChildren = newVnode.children || []

    children.forEach(function (childVnode, index) {
    var newChildVnode = newChildren[index]
    if (childVnode.tag === newChildVnode.tag) {
    // 深层次对比,递归
    updateChildren(childVnode, newChildVnode)
    } else {
    // 替换
    replaceNode(childVnode, newChildVnode)
    }
    })
    }

    function replaceNode(vnode, newVnode) {
    var elem = vnode.elem // 真实的 DOM 节点
    var newElem = createElement(newVnode)

    // 替换
    }
  • diff是 Linux的基础命令

  • 为了找出需要更新的节点
  • diff实现 patch(vontainer, vnode) patch(vnode, newVnode)
  • 核心逻辑:createElement updateChildren